// Copyright (c) 2020 Presto Labs Pte. Ltd.
// Author: linchuan

#include "coin2/exchange/gdax/order_v1/parser.h"

#include <memory>
#include <set>
#include <string>
#include <utility>
#include <vector>

#include <rapidjson/document.h>

#include "coin/proto/coin_query.pb.h"
#include "coin2/base/conversion.h"
#include "coin2/base/log.h"
#include "coin2/exchange/gdax/api_order/native_private_client.h"
#include "coin2/exchange/gdax/api_util/transfers_account_store.h"
#include "coin2/exchange/util/json_util.h"
#include "quickfix/Message.h"

namespace coin2::exchange::gdax::order_v1 {

namespace {

using coin::proto::AccountBalance;
using coin::proto::AccountPosition;
using coin::proto::CancelOrderResponse;
using coin::proto::CurrencyTransferBundle;
using coin::proto::CurrencyTransferElement;
using coin::proto::FillType;
using coin::proto::ProductFillBundle;
using coin::proto::ProductOrderBundle;
using coin::proto::ProductOrderElement;
using coin::proto::SubmitOrderResponse;
using coin::proto::TransferRequestProto;

using base::order::IOrderContext;
using symbology::GdaxCurrency;
using symbology::GdaxProduct;

using JsonValue = rapidjson::GenericValue<rapidjson::UTF8<>>;
using GdaxOrderSubTopicId = coin2::exchange::gdax::api_order::GdaxOrderSubTopicId;
using coin2::exchange::gdax::api_order::GdaxTransfersAccountStore;

inline int64_t GetProcOrderId(const JsonValue& data) {
  int64_t proc_order_id = 0;
  if (data.HasMember("client_oid") && !std::string(data["client_oid"].GetString()).empty()) {
    std::string client_oid_str = std::string(data["client_oid"].GetString());
    boost::replace_all(client_oid_str, "-", "");
    try {
      proc_order_id = stoll(client_oid_str.substr(13, 19));
    } catch (const std::exception& e) {
      // ignore
    }
  }
  return proc_order_id;
}

inline std::string GetProcOrderId(const std::string& client_id_str) {
  if (client_id_str.empty()) {
    return client_id_str;
  }
  std::string client_oid_str = client_id_str;
  boost::replace_all(client_oid_str, "-", "");
  return client_oid_str.substr(13, 19);
}

inline coin::proto::OrderSide GetOrderSide(const std::string& side) {
  if (side == "buy") {
    return coin::proto::OrderSide::BUY_ORDER;
  } else if (side == "sell") {
    return coin::proto::OrderSide::SELL_ORDER;
  } else {
    return coin::proto::OrderSide::UNKNOWN_ORDER_SIDE;
  }
}

inline coin::proto::OrderType GetOrderType(const std::string& type) {
  if (type == "limit") {
    return coin::proto::OrderType::LIMIT_ORDER;
  } else if (type == "market") {
    return coin::proto::OrderType::MARKET_ORDER;
  } else {
    return coin::proto::OrderType::UNKNOWN_ORDER_TYPE;
  }
}

inline void SetOrderStatus(const JsonValue& data, ProductOrderElement* order) {
  std::string status = data["status"].GetString();
  if (status == "received" || status == "rejected") {
    order->set_is_live(false);
  } else if (status == "open") {
    order->set_is_live(true);
  } else if (status == "done") {
    std::string done_reason = data["done_reason"].GetString();
    if (done_reason == "canceled") {
      order->set_cancel_confirmed(true);
    } else if (status == "filled") {
      order->set_fully_filled(true);
    }
  } else if (status == "pending") {
    // linchuan: pending may received after open, so do nothing.
  } else {
    LOG(ERROR) << "[ParseRestOrderInfo] Unknown order status! " << status;
  }
}

inline bool IsGdaxErrMessage(const JsonValue& doc) { return doc.HasMember("message"); }

inline std::string GetGdaxExchangeOrderErrorMsg(const JsonValue& json) {
  return json["message"].GetString();
}

inline coin::proto::OrderErrorCode GetOrderErrorCodeFromGdax(
    const JsonValue& json_error_msg,
    bool has_exchange_order_id) {
  using coin::proto::OrderErrorCode;
  auto result = coin::proto::OrderErrorCode::ORDER_UNKNOWN_ERROR;
  std::string error_code(json_error_msg.GetString());

  if (error_code == "0") {
    result = OrderErrorCode::ORDER_NO_ERROR;
  } else if (error_code == "Insufficient funds") {
    result = OrderErrorCode::SUBMIT_ORDER_INSUFFICIENT_FUND;
  } else if (error_code == "order not found") {
    result = OrderErrorCode::ORDER_ON_THE_FLY;
  } else if (error_code == "Post only mode") {
    result = OrderErrorCode::POST_ONLY_REJECT;
  } else if (error_code == "Private rate limit exceeded") {
    result = OrderErrorCode::API_LIMIT_ERROR;
  } else {
    LOG(ERROR) << "Unknown error code" << error_code;
  }

  return result;
}

bool IsOrderUpdate(const rapidjson::GenericValue<rapidjson::UTF8<>>& json) {
  if (!json.HasMember("type")) {
    return false;
  }

  std::string ws_type = json["type"].GetString();
  return ws_type == "received" || ws_type == "open" || ws_type == "done" || ws_type == "match";
}

}  // namespace

void GdaxParser::ParseRestAccountBalance() {
  JsonDocument document;
  if (!PrepareJsonDocument(&document)) {
    LOG(ERROR) << "[ParseRestAccountBalance] Parsing Error. " << msg_->DebugString();
    result_.set_error_code(OrderErrorCode::INVALID_JSON);
    return;
  }

  auto account_balance = ParseBalance(document);
  if (!account_balance.has_exchange() || !account_balance.has_market_type()) {
    result_.set_error_code(OrderErrorCode::INVALID_JSON);
    return;
  }
  result_.set_type(MsgType::ACCOUNT_BALANCE);
  *result_.mutable_account_balance() = std::move(account_balance);
}

// NOTE: when field exchange and market_type are set, which means parsed successfully
AccountBalance GdaxParser::ParseBalance(const JsonValue& doc) {
  RawBalanceMap raw_balance_map;
  auto success = ParseRawBalance(doc, &raw_balance_map);
  if (!success) {
    return {};
  }

  AccountBalance acc_balance;
  acc_balance.set_exchange("Gdax");
  acc_balance.set_market_type("Spot");
  for (const auto& native_currency : symcache_->GetCurrenciesNativeUpper()) {
    auto* balance = acc_balance.add_each_balance();
    if (raw_balance_map.count(native_currency) == 0) {
      balance->set_total(0);
      balance->set_hold(0);
      balance->set_available(0);
    } else {
      balance->MergeFrom(raw_balance_map.at(native_currency));
    }
    auto* currency = symcache_->GetCurrencyByNative(native_currency);
    balance->set_currency(currency->currency());
    balance->set_currency_native(currency->native_currency());
    DLOG(INFO) << "BALANCE: " << currency->native_currency() << " " << balance->total() << " "
               << balance->available();
  }
  is_account_balance_initialized_ = true;

  return acc_balance;
}

bool GdaxParser::ParseRawBalance(const JsonValue& doc, RawBalanceMap* raw_balance_map) {
  /*
  [
    {
      "id": "71452118-efc7-4cc4-8780-a5e22d4baa53",
      "currency": "BTC",
      "balance": "0.0000000000000000",
      "available": "0.0000000000000000",
      "hold": "0.0000000000000000",
      "profile_id": "75da88c5-05bf-4f54-bc85-5c775bd68254"
    },
    {
      "id": "e316cb9a-0808-4fd7-8914-97829c1925de",
      "currency": "USD",
      "balance": "80.2301373066930000",
      "available": "79.2266348066930000",
      "hold": "1.0035025000000000",
      "profile_id": "75da88c5-05bf-4f54-bc85-5c775bd68254"
    }
  ]
  */
  if (doc.IsObject() && IsGdaxErrMessage(doc)) {
    LOG(ERROR) << "[ParseBalance] " << JsonToString(doc);
    return false;
  }
  if (!doc.IsArray()) {
    LOG(ERROR) << "[ParseBalance] " << JsonToString(doc);
    return false;
  }

  for (const auto& data : doc.GetArray()) {
    if (!data.HasMember("currency") || !data.HasMember("balance") || !data.HasMember("available") ||
        !data.HasMember("hold")) {
      THROW() << "Parse error in account balance" << JsonToString(data);
    }

    CurrencyBalance currency_balance;
    currency_balance.set_currency_native(data["currency"].GetString());
    currency_balance.set_available(stod(data["available"].GetString()));
    currency_balance.set_hold(stod(data["hold"].GetString()));
    currency_balance.set_total(stod(data["balance"].GetString()));
    raw_balance_map->emplace(currency_balance.currency_native(), currency_balance);
  }

  return true;
}

ProductOrderElement GdaxParser::ParseRestOrderInfo(const JsonValue& data) {
  /*
  [
    {
      "id": "d0c5340b-6d6c-49d9-b567-48c4bfca13d2",
      "price": "0.10000000",
      "size": "0.01000000",
      "product_id": "BTC-USD",
      "side": "buy",
      "stp": "dc",
      "type": "limit",
      "time_in_force": "GTC",
      "post_only": false,
      "created_at": "2016-12-08T20:02:28.53864Z",
      "fill_fees": "0.0000000000000000",
      "filled_size": "0.00000000",
      "executed_value": "0.0000000000000000",
      "status": "open",
      "settled": false
    }
  ]
  */
  ProductOrderElement order;
  order.set_market_type(MarketType::Spot);
  order.set_exchange_type(ExchangeType::Gdax);
  order.set_api_version("v1");
  order.set_exchange_order_id(data["id"].GetString());
  order.set_symbol(data["product_id"].GetString());
  order.set_price(stod(data["price"].GetString()));
  order.set_qty(stod(data["size"].GetString()));
  order.set_proc_order_id(GetProcOrderId(data));
  order.set_side(GetOrderSide(data["side"].GetString()));
  order.set_order_type(GetOrderType(data["type"].GetString()));
  order.set_timestamp(msg_->timestamp);

  if (data.HasMember("created_at")) {
    order.set_created_timestamp(TimestampFromIso8601(data["created_at"].GetString()));
  }

  SetOrderStatus(data, &order);
  return order;
}

void GdaxParser::ParseRestOrdersInfo() {
  JsonDocument data;
  if (!PrepareJsonDocument(&data)) {
    LOG(ERROR) << "[ParseRestOpenOrders] Parsing Error. " << msg_->DebugString();
    result_.set_error_code(OrderErrorCode::INVALID_JSON);
    return;
  }
  if (data.IsObject() && IsGdaxErrMessage(data)) {
    LOG(ERROR) << "[ParseRestOrderInfoList] " << JsonToString(data);
    return;
  }

  if (!data.IsArray()) {
    LOG(ERROR) << "Unexpected order list format!" << JsonToString(data);
    return;
  }

  result_.set_type(MsgType::ORDER_STATUS);
  auto product_order_bundle = result_.mutable_order_list();
  product_order_bundle->Clear();
  for (const auto& info : data.GetArray()) {
    if (!info.HasMember("product_id")) {
      continue;
    }
    std::string symbol_str = info["product_id"].GetString();
    if (!symcache_->has_product_native(symbol_str)) {
      continue;
    }
    auto new_order = ParseRestOrderInfo(info);
    auto oc = GetOcm()->FindByExchangeOrderId(new_order.exchange_order_id());

    // If order_context state is dead but exchange order info shows it is alive,
    // print warning and ignore.
    if (oc && (!oc->is_alive()) && new_order.is_live()) {
      // Ingore this order order info if order status is not consistent.
      LOG_EVERY_N(ERROR, 10) << "Gdax wrong rest open order list: \n"
                             << JsonToString(data) << "\n"
                             << new_order.DebugString();
      continue;
    }
    auto* order = product_order_bundle->add_each_order();
    order->MergeFrom(new_order);
  }
}

ProductOrderBundle GdaxParser::ParseWsOrderInfoListV1(const JsonValue& data) {
  /*

  {
    "type": "received",
    "side": "sell",
    "product_id": "BTC-USD",
    "time": "2020-04-08T06:18:48.139273Z",
    "sequence": 13387603041,
    "profile_id": "479c3da8-d58e-49ba-96c4-2044e8e6fe56",
    "user_id": "5c64424a96e3d4000ea452ad",
    "order_id": "5517d3cc-2261-42d0-a156-6638029236fd",
    "order_type": "limit",
    "size": "0.001",
    "price": "7351.62",
    "client_oid": ""
  }

  {
    "type": "open",
    "side": "sell",
    "product_id": "BTC-USD",
    "time": "2020-04-17T05:43:51.463597Z",
    "sequence": "13515451131",
    "profile_id": "f756948d-21b8-4e44-8aa3-67f5e26f085b",
    "user_id": "5c64424a96e3d4000ea452ad",
    "price": "7082.63",
    "order_id": "16c736c4-48c0-4f95-9023-8079489a0128",
    "remaining_size": "0.001"
  }
  */
  ProductOrderBundle product_order_bundle;

  auto* order = product_order_bundle.add_each_order();
  order->set_market_type(MarketType::Spot);
  order->set_exchange_type(ExchangeType::Gdax);
  order->set_api_version("v1");
  order->set_exchange_order_id(data["order_id"].GetString());
  order->set_proc_order_id(GetProcOrderId(data));
  order->set_symbol(data["product_id"].GetString());
  order->set_price(stod(data["price"].GetString()));

  if (data.HasMember("order_type")) {
    order->set_order_type(GetOrderType(data["order_type"].GetString()));
    order_type_map_[order->exchange_order_id()] = order->order_type();
  } else if (order_type_map_.count(order->exchange_order_id()) == 1) {
    order->set_order_type(order_type_map_.at(order->exchange_order_id()));
  } else {
    // Ignore order, let rest query handle this case;
    return product_order_bundle;
  }

  if (data.HasMember("size")) {
    order->set_qty(stod(data["size"].GetString()));
    order_qty_map_[order->exchange_order_id()] = order->qty();
  } else if (order_qty_map_.count(order->exchange_order_id()) == 1) {
    order->set_qty(order_qty_map_.at(order->exchange_order_id()));
  } else {
    // Ignore order, let rest query handle this case;
    return product_order_bundle;
  }

  order->set_side(GetOrderSide(data["side"].GetString()));
  order->set_timestamp(msg_->timestamp);

  std::string status(data["type"].GetString());
  if (status == "rejected") {
    order->set_is_live(false);
  } else if (status == "received" || status == "open") {
    order->set_is_live(true);
  } else if (status == "done") {
    std::string done_reason = data["reason"].GetString();
    if (done_reason == "canceled") {
      order->set_cancel_confirmed(true);
    } else if (status == "filled") {
      order->set_fully_filled(true);
    }
  } else {
    LOG(ERROR) << "[ParseRestOrderInfo] Unknown order status! " << status;
  }
  return product_order_bundle;
}

void GdaxParser::SetProductFillBundleFromRaw(
    const rapidjson::GenericValue<rapidjson::UTF8<>>& data,
    coin::proto::ProductFillBundle* product_fill_bundle) {
  /*
  [
    {
      "trade_id": 74,
      "product_id": "BTC-USD",
      "price": "10.00",
      "size": "0.01",
      "order_id": "d50ec984-77a8-460a-b958-66f114b0de9b",
      "created_at": "2014-11-07T22:19:28.578544Z",
      "liquidity": "T",
      "fee": "0.00025",
      "settled": true,
      "side": "buy"
    }
  ]
  */
  if (!(data.IsArray() && data.Size() > 0)) {
    return;
  }

  for (const auto& info : data.GetArray()) {
    // linchuan: ignore the fills long ago
    int64_t fill_timestamp_et = TimestampFromIso8601(info["created_at"].GetString());
    int64_t fill_timestamp_ft = msg_->timestamp;
    if (fill_timestamp_ft - fill_timestamp_et > 100 * 1e9) {
      continue;
    }
    std::string sybmol_str = info["product_id"].GetString();
    if (!symcache_->has_product_native(sybmol_str)) {
      continue;
    }

    auto* fill = product_fill_bundle->add_each_fill();
    fill->set_symbol(info["product_id"].GetString());
    fill->set_fill_timestamp(fill_timestamp_ft);
    fill->set_exchange_order_id(sybmol_str);
    fill->set_price(stod(info["price"].GetString()));
    fill->set_qty(stod(info["size"].GetString()));
    fill->set_fill_id(info["trade_id"].GetString());
    if (info["liquidity"].GetString()[0] == 'M') {
      fill->set_fill_type(FillType::MAKER_FILL_TYPE);
    } else if (info["liquidity"].GetString()[0] == 'T') {
      fill->set_fill_type(FillType::TAKER_FILL_TYPE);
    } else {
      fill->set_fill_type(FillType::UNKNOWN_FILL_TYPE);
    }
    fill->set_fee(stod(info["fee"].GetString()));
  }
  return;
}

void GdaxParser::ParseRestFillOrders() {
  JsonDocument data;
  if (!PrepareJsonDocument(&data)) {
    LOG(ERROR) << "[ParseRestFillOrders] Parsing Error. " << msg_->DebugString();
    result_.set_error_code(OrderErrorCode::INVALID_JSON);
    return;
  }
  if (data.IsObject() && IsGdaxErrMessage(data)) {
    LOG(ERROR) << "[ParseRestFillInfoList] " << JsonToString(data);
    return;
  }
  result_.set_type(MsgType::ORDER_FILL);
  auto product_fill_bundle = result_.mutable_fill_list();
  SetProductFillBundleFromRaw(data, product_fill_bundle);
}

ProductFillBundle GdaxParser::ParseWsFillInfoListV1(const JsonValue& data) {
  /*
  {
    "type": "match",
    "side": "sell",
    "product_id": "BTC-USD",
    "time": "2020-04-09T02:17:26.809448Z",
    "sequence": "13401175362",
    "profile_id": "479c3da8-d58e-49ba-96c4-2044e8e6fe56",
    "user_id": "5c64424a96e3d4000ea452ad",
    "trade_id": "88072169",
    "maker_order_id": "409e2325-964f-4617-91b6-a4c24bdfebe6",
    "taker_order_id": "31eeea00-4130-48dc-9360-6243228ffeff",
    "size": "0.001",
    "price": "7328.81",
    "maker_profile_id": "479c3da8-d58e-49ba-96c4-2044e8e6fe56",
    "maker_user_id": "5c64424a96e3d4000ea452ad"
    // Or, above could be 'taker_profile_id' or 'taker_user_id'
  }
  */
  ProductFillBundle product_fill_bundle;
  auto* fill = product_fill_bundle.add_each_fill();
  fill->set_symbol(data["product_id"].GetString());
  fill->set_fill_timestamp(msg_->timestamp);
  fill->set_price(stod(data["price"].GetString()));
  fill->set_qty(stod(data["size"].GetString()));
  fill->set_fill_id(data["trade_id"].GetString());
  if (data.HasMember("maker_user_id") && std::string_view(data["user_id"].GetString()) ==
                                             std::string_view(data["maker_user_id"].GetString())) {
    fill->set_fill_type(FillType::MAKER_FILL_TYPE);
    fill->set_exchange_order_id(data["maker_order_id"].GetString());
  } else {
    fill->set_fill_type(FillType::TAKER_FILL_TYPE);
    fill->set_exchange_order_id(data["taker_order_id"].GetString());
  }
  return product_fill_bundle;
}

void GdaxParser::ParseSubmitResponse() {
  const auto order_context = msg_->oc;
  const auto& http_context = msg_->http_context;
  if (!IsValidResponse(http_context)) {
    return;
  }

  result_.set_type(MsgType::SUBMIT_RESPONSE);
  auto resp = result_.mutable_submit_response();
  resp->Clear();
  resp->set_symbol(order_context->order_spec().product);
  resp->set_proc_order_id(order_context->proc_order_id());
  resp->set_http_status(http_context->res.result_int());
  resp->set_timestamp(msg_->timestamp);

  if (http_context->status == AsioHttpContextStatus::kError) {
    LOG(ERROR) << "[GdaxSubmitOrder] AsioHttpContextStatus Error";
    resp->set_error_code(coin::proto::OrderErrorCode::ORDER_NETWORK_ERROR);
    resp->set_success(false);
    return;
  }

  if (http_context->res.result_int() == 429) {
    LOG(ERROR) << "[GdaxSubmitOrder] API_LIMIT_ERROR";
    resp->set_error_code(coin::proto::OrderErrorCode::API_LIMIT_ERROR);
    resp->set_success(false);
    return;
  }

  JsonDocument json;
  if (!PrepareJsonDocument(&json)) {
    LOG(ERROR) << "[BithumbSubmitOrder] Parse Error\n" << http_context->res;
    resp->set_error_code(coin::proto::OrderErrorCode::ORDER_API_ERROR);
    resp->set_success(false);
    return;
  }

  if (json.HasMember("message")) {
    auto error_code =
        GetOrderErrorCodeFromGdax(json["message"], order_context->has_exchange_order_id());
    resp->set_error_code(error_code);
    resp->set_exchange_error_msg(GetGdaxExchangeOrderErrorMsg(json));
    if (error_code != coin::proto::OrderErrorCode::ORDER_NO_ERROR) {
      if (error_code != coin::proto::OrderErrorCode::POST_ONLY_REJECT) {
        LOG(ERROR) << "[GdaxSubmitOrder] Error Response" << http_context->res;
      }
    }
    resp->set_success(false);
    return;
  }

  resp->set_exchange_order_id(json["id"].GetString());
  resp->set_success(true);
}

void GdaxParser::ParseCancelResponse() {
  const auto order_context = msg_->oc;
  const auto& http_context = msg_->http_context;
  if (!IsValidResponse(http_context)) {
    return;
  }

  result_.set_type(MsgType::CANCEL_RESPONSE);
  auto resp = result_.mutable_cancel_response();
  resp->Clear();
  resp->set_symbol(order_context->order_spec().product);
  resp->set_proc_order_id(order_context->proc_order_id());
  resp->set_http_status(http_context->res.result_int());
  resp->set_exchange_order_id(order_context->exchange_order_id());
  resp->set_timestamp(msg_->timestamp);

  if (http_context->status == AsioHttpContextStatus::kError) {
    LOG(ERROR) << "[GdaxCancelOrder] AsioHttpContextStatus Error";
    resp->set_error_code(coin::proto::OrderErrorCode::ORDER_NETWORK_ERROR);
    resp->set_success(false);
    return;
  }

  if (http_context->res.result_int() == 429) {
    LOG(ERROR) << "[GdaxCancelOrder] API_LIMIT_ERROR";
    resp->set_error_code(coin::proto::OrderErrorCode::API_LIMIT_ERROR);
    resp->set_success(false);
    return;
  }

  JsonDocument json;
  if (!PrepareJsonDocument(&json)) {
    LOG(ERROR) << "[BithumbCancelOrder] Parse Error\n" << http_context->res;
    resp->set_error_code(coin::proto::OrderErrorCode::ORDER_API_ERROR);
    resp->set_success(false);
    return;
  }

  if (json.IsObject()) {
    auto error_code =
        GetOrderErrorCodeFromGdax(json["message"], order_context->has_exchange_order_id());
    resp->set_error_code(error_code);
    resp->set_exchange_error_msg(GetGdaxExchangeOrderErrorMsg(json));
    if (error_code != coin::proto::OrderErrorCode::ORDER_NO_ERROR) {
      LOG(ERROR) << "[GdaxCancelOrder] Error Response" << http_context->res;
    }
    resp->set_success(false);
    return;
  }

  if (!json.IsString()) {
    LOG(ERROR) << "Unexpected response!" << http_context->res;
    resp->set_success(false);
    return;
  }

  resp->set_success(true);
  resp->set_cancel_confirmed(true);
}

void GdaxParser::ParseMultiCancelResponse() {
  const auto order_context = msg_->oc;
  const auto& http_context = msg_->http_context;
  if (!IsValidResponse(http_context)) {
    return;
  }

  result_.set_type(MsgType::CANCEL_RESPONSE);
  auto resp = result_.mutable_cancel_response();
  resp->Clear();
  resp->set_symbol(order_context->order_spec().product);
  resp->set_proc_order_id(order_context->proc_order_id());
  resp->set_http_status(http_context->res.result_int());
  resp->set_exchange_order_id(order_context->exchange_order_id());
  resp->set_timestamp(msg_->timestamp);

  if (http_context->status == AsioHttpContextStatus::kError) {
    LOG(ERROR) << "[GdaxMultiCancelOrder] AsioHttpContextStatus Error";
    resp->set_error_code(coin::proto::OrderErrorCode::ORDER_NETWORK_ERROR);
    resp->set_success(false);
    return;
  }

  if (http_context->res.result_int() == 429) {
    LOG(ERROR) << "[GdaxMultiCancelOrder] API_LIMIT_ERROR";
    resp->set_error_code(coin::proto::OrderErrorCode::API_LIMIT_ERROR);
    resp->set_success(false);
    return;
  }

  JsonDocument json;
  if (!PrepareJsonDocument(&json)) {
    LOG(ERROR) << "[UpbitCancelOrder] Parsing Error." << msg_->DebugString();
    resp->set_error_code(coin::proto::OrderErrorCode::ORDER_API_ERROR);
    resp->set_success(false);
    return;
  }

  if (json.IsObject() && json.HasMember("message")) {
    auto error_code =
        GetOrderErrorCodeFromGdax(json["message"], order_context->has_exchange_order_id());
    resp->set_error_code(error_code);
    resp->set_exchange_error_msg(GetGdaxExchangeOrderErrorMsg(json));
    if (error_code != coin::proto::OrderErrorCode::ORDER_NO_ERROR) {
      LOG(ERROR) << "[GdaxMultiCancelOrder] Error Response" << http_context->res;
    }
    resp->set_success(false);
    return;
  }
  if (!json.IsArray()) {
    resp->set_success(false);
    return;
  }
  bool find_exchange_order_id = false;
  for (unsigned i = 0; i < json["order_ids"].Size(); i++) {
    if (order_context->exchange_order_id() == json["order_ids"][i].GetString()) {
      find_exchange_order_id = true;
      break;
    }
  }
  if (!find_exchange_order_id) {
    resp->set_success(false);
    return;
  }
  resp->set_success(true);
}

void GdaxParser::ParseRestDeposit() {
  /*
  [
    {
      "id": "21d2c0a0-609c-4dad-bd03-7d7e38970182",
      "type": "deposit",
      "created_at": "2022-07-14 07:07:15.180598+00",
      "completed_at": "2022-07-14 07:07:15.198542+00",
      "account_id": "0ff2c816-d3b0-4991-b3a4-d2f23ea609be",
      "user_id": "5c64424a96e3d4000ea452ad",
      "amount": "4901.75000000",
      "details": {
        "coinbase_account_id": "f3c1c605-7d7c-5294-adc1-7c019651df83",
        "tx_service_transaction_id": "516499683",
        "coinbase_payment_method_id": ""
      },
      "canceled_at": null,
      "processed_at": "2022-07-14 07:07:15.198542+00",
      "user_nonce": null,
      "idem": "b9f119a0-f3ff-5c2f-8f60-e8d0b1a43971",
      "profile_id": "f756948d-21b8-4e44-8aa3-67f5e26f085b",
      "currency": "BOBA"
    },
  ]
  */
  JsonDocument document;
  if (!PrepareJsonDocument(&document)) {
    LOG(ERROR) << "[ParseRestDeposit] Parsing error\n" << msg_->DebugString();
    result_.set_error_code(OrderErrorCode::INVALID_JSON);
    return;
  }

  if (document.IsObject() && IsGdaxErrMessage(document)) {
    LOG(ERROR) << "[ParseRestDeposit] Error message\n" << JsonToString(document);
    result_.set_error_code(OrderErrorCode::INVALID_JSON);
    return;
  }

  if (!document.IsArray()) {
    LOG(ERROR) << "[ParseRestDeposit] Invalid data format\n" << JsonToString(document);
    result_.set_error_code(OrderErrorCode::INVALID_JSON);
    return;
  }

  auto currency_transfer_bundle = result_.mutable_currency_transfer_bundle();
  currency_transfer_bundle->Clear();
  bool currency_transfer_bundle_found = false;
  for (const auto& data : document.GetArray()) {
    if (!data.HasMember("type") || !data.HasMember("amount") || !data.HasMember("created_at")) {
      LOG(ERROR) << "[ParseRestDeposit] Invalid transfer format" << JsonToString(data);
      continue;
    }
    if (!data.HasMember("currency") && !data.HasMember("account_id")) {
      LOG(ERROR) << "[ParseRestDeposit] Invalid transfer format" << JsonToString(data);
      continue;
    }
    std::string currency_str;
    if (data.HasMember("currency")) {
      currency_str = data["currency"].GetString();
    } else if (data.HasMember("account_id")) {
      auto currency_id_map = GdaxTransfersAccountStore::GetInstance().GetCurrencyIdMap();
      currency_str = currency_id_map[data["account_id"].GetString()];
    }
    if (currency_str.size() == 0) {
      LOG(ERROR) << "[ParseRestDeposit] Invalid transfer format" << JsonToString(data);
      continue;
    }

    if (std::string(data["type"].GetString()) != "deposit") {
      continue;
    }

    std::string transfer_id = data["id"].GetString();
    if (transfer_deposit_map_.count(transfer_id) <= 0) {
      transfer_deposit_map_[transfer_id] = "";
    }
    std::string state;
    if (!data["completed_at"].IsNull()) {
      state = "completed";
    } else if (!data["canceled_at"].IsNull()) {
      state = "canceled";
    } else if (!data["processed_at"].IsNull()) {
      state = "processed";
    } else if (!data["created_at"].IsNull()) {
      state = "created";
    } else {
      continue;
    }
    if (!transfer_first_record_) {
      if (state == transfer_deposit_map_[transfer_id]) {
        continue;
      }
    }
    transfer_deposit_map_[transfer_id] = state;

    auto* currency_transfer = currency_transfer_bundle->add_each_transfer();
    currency_transfer->set_type(TransferRequestProto::DEPOSIT);
    if (state == "created") {
      currency_transfer->set_status(TransferRequestProto::TRANSFER_SUBMITTED);
    } else if (state == "processed") {
      currency_transfer->set_status(TransferRequestProto::TRANSFER_ACCEPTED);
    } else if (state == "canceled") {
      currency_transfer->set_status(TransferRequestProto::TRANSFER_REJECTED);
    } else if (state == "completed") {
      currency_transfer->set_status(TransferRequestProto::TRANSFER_CONFIRMED);
    }
    currency_transfer->set_external_transfer_id(transfer_id);
    currency_transfer->set_qty(stod(data["amount"].GetString()));
    if (data.HasMember("details")) {
      if (data["details"].HasMember("crypto_address")) {
        currency_transfer->set_from_address(data["details"]["crypto_address"].GetString());
      } else if (data["details"].HasMember("coinbase_account_id")) {
        currency_transfer->set_from_address(data["details"]["coinbase_account_id"].GetString());
      }
    }

    int64_t create_timestamp = TimestampFromGdaxIso8601(data["created_at"].GetString());
    currency_transfer->set_create_timestamp(create_timestamp);
    auto currency = GdaxCurrency::FromNativeStrImpl(currency_str, create_timestamp);
    currency_transfer->set_currency(currency->currency());
    currency_transfer_bundle_found = true;
  }

  if (!transfer_first_record_) {
    if (!is_transfer_deposit_initialized_) {
      is_transfer_deposit_initialized_ = true;
      LOG(INFO) << "[ParseRestDeposit] Deposit ignored: " << transfer_deposit_map_.size();
      return;
    }
  }

  if (currency_transfer_bundle_found) {
    LOG(INFO) << "[ParseRestDeposit] Deposit parsed: "
              << currency_transfer_bundle->each_transfer_size();
    result_.set_type(MsgType::TRANSFER);
  }
}

void GdaxParser::ParseRestWithdrawal() {
  /*
  [
    {
      "id": "ec2902f2-788c-4d1c-8bf3-cdedbc494abcd",
      "type": "withdraw",
      "created_at": "2019-07-19 09:16:52.233783+00",
      "completed_at": null,
      "canceled_at": "2019-07-19 09:16:54.144667+00",
      "processed_at": null,
      "account_id": "091eb6a4-c830-40f7-8c53-e69859efabcd",
      "user_id": "5c64424a96e3d4000ea4abcd",
      "user_nonce": "1626686113252",
      "amount": "0.10046200",
      "details": {
        "fee": "0.000462",
        "subtotal": "0.1",
        "cancel_code": "0",
        "sent_to_address": "0xb6fc1aa6b4640ccedcb81c2908f57dff7c0dabcd",
        "coinbase_account_id": "d8335f96-4794-5dca-8188-bfbf765aabcd",
        "coinbase_payment_method_id": ""
      },
      "idem": null
    },
  ]
  */
  JsonDocument document;
  if (!PrepareJsonDocument(&document)) {
    LOG(ERROR) << "[ParseRestWithdrawal] Parsing error\n" << msg_->DebugString();
    result_.set_error_code(OrderErrorCode::INVALID_JSON);
    return;
  }

  if (document.IsObject() && IsGdaxErrMessage(document)) {
    LOG(ERROR) << "[ParseRestWithdrawal] Error message\n" << JsonToString(document);
    result_.set_error_code(OrderErrorCode::INVALID_JSON);
    return;
  }

  if (!document.IsArray()) {
    LOG(ERROR) << "[ParseRestWithdrawal] Invalid data format\n" << JsonToString(document);
    result_.set_error_code(OrderErrorCode::INVALID_JSON);
    return;
  }

  auto currency_transfer_bundle = result_.mutable_currency_transfer_bundle();
  currency_transfer_bundle->Clear();
  bool currency_transfer_bundle_found = false;
  for (const auto& data : document.GetArray()) {
    if (!data.HasMember("type") || !data.HasMember("amount") || !data.HasMember("created_at")) {
      LOG(ERROR) << "[ParseRestWithdrawal] Invalid transfer format" << JsonToString(data);
      continue;
    }

    if (!data.HasMember("currency") && !data.HasMember("account_id")) {
      LOG(ERROR) << "[ParseRestDeposit] Invalid transfer format" << JsonToString(data);
      continue;
    }
    std::string currency_str;
    if (data.HasMember("currency")) {
      currency_str = data["currency"].GetString();
    } else if (data.HasMember("account_id")) {
      auto currency_id_map = GdaxTransfersAccountStore::GetInstance().GetCurrencyIdMap();
      currency_str = currency_id_map[data["account_id"].GetString()];
    }
    if (currency_str.size() == 0) {
      LOG(ERROR) << "[ParseRestDeposit] Invalid transfer format" << JsonToString(data);
    }

    if (std::string(data["type"].GetString()) != "withdraw") {
      continue;
    }

    std::string transfer_id = data["id"].GetString();
    if (transfer_withdrawal_map_.count(transfer_id) <= 0) {
      transfer_withdrawal_map_[transfer_id] = "";
    }
    std::string state;
    if (!data["completed_at"].IsNull()) {
      state = "completed";
    } else if (!data["canceled_at"].IsNull()) {
      state = "canceled";
    } else if (!data["processed_at"].IsNull()) {
      state = "processed";
    } else if (!data["created_at"].IsNull()) {
      state = "created";
    } else {
      continue;
    }

    if (!transfer_first_record_) {
      if (state == transfer_withdrawal_map_[transfer_id]) {
        continue;
      }
    }
    transfer_withdrawal_map_[transfer_id] = state;

    auto* currency_transfer = currency_transfer_bundle->add_each_transfer();
    currency_transfer->set_type(TransferRequestProto::WITHDRAW);
    if (state == "created") {
      currency_transfer->set_status(TransferRequestProto::TRANSFER_SUBMITTED);
    } else if (state == "processed") {
      currency_transfer->set_status(TransferRequestProto::TRANSFER_ACCEPTED);
    } else if (state == "canceled") {
      currency_transfer->set_status(TransferRequestProto::TRANSFER_REJECTED);
    } else if (state == "completed") {
      currency_transfer->set_status(TransferRequestProto::TRANSFER_CONFIRMED);
    }
    currency_transfer->set_external_transfer_id(transfer_id);
    currency_transfer->set_qty(stod(data["amount"].GetString()));
    if (data.HasMember("details")) {
      if (data["details"].HasMember("sent_to_address")) {
        currency_transfer->set_to_address(data["details"]["sent_to_address"].GetString());
      } else if ((data["details"].HasMember("coinbase_account_id"))) {
        currency_transfer->set_to_address(data["details"]["coinbase_account_id"].GetString());
      }
    }

    int64_t create_timestamp = TimestampFromGdaxIso8601(data["created_at"].GetString());
    currency_transfer->set_create_timestamp(create_timestamp);
    auto currency = GdaxCurrency::FromNativeStrImpl(currency_str, create_timestamp);
    currency_transfer->set_currency(currency->currency());
    currency_transfer_bundle_found = true;
  }

  if (!transfer_first_record_) {
    if (!is_transfer_withdrawal_initialized_) {
      is_transfer_withdrawal_initialized_ = true;
      LOG(INFO) << "[ParseRestWithdrawal] Withdrawal ignored: " << transfer_withdrawal_map_.size();
      return;
    }
  }

  if (currency_transfer_bundle_found) {
    LOG(INFO) << "[ParseRestWithdrawal] Withdrawal parsed: "
              << currency_transfer_bundle->each_transfer_size();
    result_.set_type(MsgType::TRANSFER);
  }
}

void GdaxParser::ParseWsMessage() {
  JsonDocument document;
  if (!PrepareJsonDocument(&document)) {
    LOG(ERROR) << "[ParseWsMessage] Parsing Error. " << msg_->DebugString();
    result_.set_error_code(OrderErrorCode::INVALID_JSON);
    return;
  }

  if (IsOrderUpdate(document)) {
    std::string ws_type = document["type"].GetString();
    if (ws_type == "match") {
      *result_.mutable_fill_list() = ParseWsFillInfoListV1(document);
      result_.set_type(MsgType::ORDER_FILL);
    } else {
      *result_.mutable_order_list() = ParseWsOrderInfoListV1(document);
      result_.set_type(MsgType::ORDER_STATUS);
    }
  }
}

void GdaxParser::ParseFixMessage() {
  if (!msg_ || msg_->fix_message == nullptr) {
    return;
  }
  const FIX::Message& message = *(msg_->fix_message);
  DLOG(INFO) << message;
  FIX::MsgType msg_type;
  if (!message.getHeader().getFieldIfSet(msg_type)) {
    return;
  }
  if (MSG_TYPE_SESSION_REJECT == msg_type) {
    // TODO(daniel) - session reject
  } else if (MSG_TYPE_EXECUTION_REPORT == msg_type) {
    ParseFixSubmitResponse(message);
    ParseFixCancelResponse(message);
    ParseFixFillInfo(message);
    ParseFixOrderInfo(message);
  } else if (MSG_TYPE_ORDER_CANCEL_REJECT == msg_type) {
    ParseFixCancelResponse(message);
  }
}

void GdaxParser::ParseFixSubmitResponse(const FIX::Message& message) {
  FIX::ExecType exec_type;
  if (!message.getFieldIfSet(exec_type)) {
    return;
  }
  if (EXEC_TYPE_REJECTED != exec_type) {
    return;
  }
  std::optional<int64_t> proc_order_id;
  FIX::ClOrdID client_id;
  if (message.getFieldIfSet(client_id)) {
    proc_order_id = ToInt64(GetProcOrderId(client_id.getValue()));
  }
  if (!proc_order_id) {
    LOG(ERROR) << "proc_order_id not found. " << message;
    return;
  }
  FIX::Symbol symbol;
  message.getFieldIfSet(symbol);

  SubmitOrderResponse& resp = *result_.mutable_submit_response();
  resp.Clear();
  resp.set_symbol(symbol.getValue());
  resp.set_proc_order_id(*proc_order_id);
  resp.set_timestamp(msg_->timestamp);
  resp.set_error_code(coin::proto::OrderErrorCode::ORDER_UNKNOWN_ERROR);
  FIX::OrdRejReason error_code;
  if (message.getFieldIfSet(error_code)) {
    if (3 == error_code) {
      resp.set_error_code(coin::proto::OrderErrorCode::SUBMIT_ORDER_INSUFFICIENT_FUND);
    } else if (8 == error_code) {
      resp.set_error_code(coin::proto::OrderErrorCode::POST_ONLY_REJECT);
    }
  }
  FIX::Text error_reason;
  if (message.getFieldIfSet(error_reason)) {
    resp.set_exchange_error_msg(error_reason.getValue());
  }
  resp.set_success(false);
  result_.set_type(MsgType::SUBMIT_RESPONSE);
}

void GdaxParser::ParseFixCancelResponse(const FIX::Message& message) {
  FIX::MsgType msg_type;
  if (!message.getHeader().getFieldIfSet(msg_type)) {
    return;
  }
  if (MSG_TYPE_ORDER_CANCEL_REJECT != msg_type) {
    return;
  }
  std::optional<int64_t> proc_order_id;
  FIX::ClOrdID client_id;
  if (message.getFieldIfSet(client_id)) {
    proc_order_id = ToInt64(GetProcOrderId(client_id.getValue()));
  }
  if (!proc_order_id) {
    LOG(ERROR) << "proc_order_id not found. " << message;
    return;
  }
  FIX::OrderID order_id;
  message.getFieldIfSet(order_id);

  CancelOrderResponse& resp = *result_.mutable_cancel_response();
  resp.Clear();
  resp.set_proc_order_id(*proc_order_id);
  resp.set_exchange_order_id(order_id.getValue());
  resp.set_timestamp(msg_->timestamp);

  resp.set_error_code(coin::proto::OrderErrorCode::ORDER_UNKNOWN_ERROR);
  FIX::CxlRejReason error_code;
  if (message.getFieldIfSet(error_code)) {
    if (1 == error_code) {
      resp.set_error_code(coin::proto::OrderErrorCode::ORDER_NOT_FOUND);
    } else {
      LOG(WARNING) << "Undocumented error code found: " << error_code
                   << ", fix_message: " << message.toString();
    }
  }
  FIX::Text error_reason;
  if (message.getFieldIfSet(error_reason)) {
    resp.set_exchange_error_msg(error_reason.getValue());
    // Error msg found in Gdax but not in documentation.
    if (error_reason == "order not open") {
      resp.set_error_code(coin::proto::OrderErrorCode::ORDER_NOT_FOUND);
    }
    if (!resp.exchange_error_msg().empty()) {
      LOG(WARNING) << "error reason: " << message.toString();
    }
  }
  resp.set_success(false);
  result_.set_type(MsgType::CANCEL_RESPONSE);
}

void GdaxParser::ParseFixFillInfo(const FIX::Message& message) {
  FIX::ExecType exec_type;
  if (!message.getFieldIfSet(exec_type)) {
    return;
  }
  if (EXEC_TYPE_FILL != exec_type) {
    return;
  }
  FIX::OrderID order_id;
  FIX::Symbol symbol;
  FIX::Price price;
  FIX::LastShares order_qty;
  FIX::TradeID fill_id;
  FIX::AggressorIndicator aggressor_ind;
  message.getFieldIfSet(order_id);
  message.getFieldIfSet(symbol);
  message.getFieldIfSet(price);
  message.getFieldIfSet(order_qty);
  message.getFieldIfSet(fill_id);
  message.getFieldIfSet(aggressor_ind);

  ProductFillBundle& product_fill_bundle = *result_.mutable_fill_list();
  product_fill_bundle.Clear();
  auto fill = product_fill_bundle.add_each_fill();
  fill->Clear();
  fill->set_symbol(symbol.getValue());
  fill->set_fill_timestamp(msg_->timestamp);
  fill->set_exchange_order_id(order_id.getValue());
  fill->set_price(price.getValue());
  fill->set_qty(order_qty.getValue());
  fill->set_fill_id(fill_id.getValue());
  if (aggressor_ind.getValue()) {
    fill->set_fill_type(FillType::TAKER_FILL_TYPE);
  } else {
    fill->set_fill_type(FillType::MAKER_FILL_TYPE);
  }
  result_.set_type(MsgType::ORDER_FILL);
}

void GdaxParser::ParseRestTransfers(const std::string& id) {
  /*
  [
    {
      "id": "21667104927",
      "amount": "16.9000000000000000",
      "balance": "16.9000000000000000",
      "created_at": "2022-07-18T07:13:54.938528Z",
      "type": "transfer",
      "details": {
        "transfer_id": "5c76fbeb-6660-42d1-816c-1714e2d61147",
        "transfer_type": "deposit"
      }
    },
    {
      "id": "22955640206",
      "amount": "500.0000000000000000",
      "balance": "1256215.4100000000000000",
      "created_at": "2022-09-16T07:47:15.935057Z",
      "type": "transfer",
      "details": {
        "from": "f76e6a9d-9083-4b05-9844-15b67c0dd059",
        "profile_transfer_id": "fe572950-62c1-4da6-a59d-b382de0f23cf",
        "to": "174c604b-9881-48c3-886d-033f44424de4"
      }
    }
  ]
  */

  JsonDocument document;
  if (!PrepareJsonDocument(&document)) {
    LOG(ERROR) << "[ParseRestTransfers] Parsing Error. " << msg_->DebugString();
    result_.set_error_code(OrderErrorCode::INVALID_JSON);
    return;
  }

  if (!document.IsArray()) {
    LOG(ERROR) << "[ParseRestTransfers] Parsing Error. " << JsonToString(document);
    result_.set_error_code(OrderErrorCode::INVALID_JSON);
    return;
  }

  auto currency_transfer_bundle = result_.mutable_currency_transfer_bundle();
  currency_transfer_bundle->Clear();
  bool currency_transfer_bundle_found = false;
  for (const auto& data : document.GetArray()) {
    const static std::vector<std::string> fields =
        {"id", "amount", "created_at", "type", "details"};
    if (!JsonFieldCheck(data, fields)) {
      LOG(ERROR) << "[ParseRestTransfers] Invalid transfer format" << JsonToString(data);
      continue;
    }
    std::string type = data["type"].GetString();
    if (type != "transfer") {
      continue;
    }

    if (!data["details"].IsObject()) {
      LOG(ERROR) << "[ParseRestTransfers] unknown transfer format" << JsonToString(data);
      continue;
    }
    std::string from, to, currency_str;
    if (data["details"].HasMember("from")) {
      from = data["details"]["from"].GetString();
    }
    if (data["details"].HasMember("to")) {
      to = data["details"]["to"].GetString();
    }
    if (from.size() == 0 && to.size() == 0) {
      continue;
    }

    const auto& currency_id_map_ = GdaxTransfersAccountStore::GetInstance().GetCurrencyIdMap();
    auto it_from = currency_id_map_.find(from);
    auto it_to = currency_id_map_.find(to);
    auto transfer_type = TransferRequestProto::WITHDRAW_SUBACCOUNT;
    if ((it_to != currency_id_map_.end() && it_from != currency_id_map_.end()) ||
        (it_to == currency_id_map_.end() && it_from == currency_id_map_.end())) {
      LOG(ERROR) << "[ParseRestTransfers] unknown transfer format" << JsonToString(data);
      continue;
    } else if (it_to != currency_id_map_.end()) {
      transfer_type = TransferRequestProto::DEPOSIT_SUBACCOUNT;
      currency_str = it_to->second;
    } else {
      transfer_type = TransferRequestProto::WITHDRAW_SUBACCOUNT;
      currency_str = it_from->second;
    }

    if (!transfer_first_record_) {
      if (transfer_subaccount_map_[data["id"].GetString()] == "done") {
        continue;
      }
    }

    int64_t create_timestamp = TimestampFromIso8601(data["created_at"].GetString());
    auto currency = GdaxCurrency::FromNativeStrImpl(currency_str, create_timestamp);
    transfer_subaccount_map_.insert_or_assign(data["id"].GetString(), "done");
    auto* currency_transfer = currency_transfer_bundle->add_each_transfer();
    currency_transfer->set_currency(currency->currency());
    currency_transfer->set_status(TransferRequestProto::TRANSFER_CONFIRMED);
    currency_transfer->set_type(transfer_type);
    currency_transfer->set_create_timestamp(create_timestamp);
    currency_transfer->set_qty(std::abs(stod(data["amount"].GetString())));
    currency_transfer->set_from_address(from);
    currency_transfer->set_to_address(to);
    currency_transfer->set_external_transfer_id(data["details"]["profile_transfer_id"].GetString());
    currency_transfer_bundle_found = true;
  }

  if (!transfer_first_record_) {
    if (!is_transfer_subaccount_initialized_set_.count(id)) {
      is_transfer_subaccount_initialized_set_.insert(id);
      LOG(INFO) << "[ParseRestTransfer] Transfer subaccount ignored: "
                << transfer_subaccount_map_.size();
      return;
    }
  }

  if (currency_transfer_bundle_found) {
    LOG(INFO) << "[ParseRestTransfers] Transfers parsed: "
              << currency_transfer_bundle->each_transfer_size();
    result_.set_type(MsgType::TRANSFER);
  }

  return;
}

void GdaxParser::ParseRestTransfersInit() {
  JsonDocument doc;
  if (!PrepareJsonDocument(&doc)) {
    LOG(ERROR) << "[ParseRestTransfersInit] Parsing Error. " << msg_->DebugString();
    result_.set_error_code(OrderErrorCode::INVALID_JSON);
    return;
  }

  if (doc.IsObject() && IsGdaxErrMessage(doc)) {
    LOG(ERROR) << "[ParseRestTransfersInit] " << JsonToString(doc);
    result_.set_error_code(OrderErrorCode::INVALID_JSON);
    return;
  }
  if (!doc.IsArray()) {
    LOG(ERROR) << "[ParseRestTransfersInit] " << JsonToString(doc);
    result_.set_error_code(OrderErrorCode::INVALID_JSON);
    return;
  }
  std::map<std::string, std::string> id_current_map;
  for (const auto& data : doc.GetArray()) {
    if (!data.HasMember("currency")) {
      THROW() << "Parse error " << JsonToString(data);
    }

    if (!GdaxTransfersAccountStore::GetInstance().is_initialized() &&
        trade_currency_set_.count(data["currency"].GetString())) {
      id_current_map.insert_or_assign(data["id"].GetString(), data["currency"].GetString());
    }
  }

  GdaxTransfersAccountStore::GetInstance().SetCurrencyIdMap(id_current_map);
}

void GdaxParser::ParseFixOrderInfo(const FIX::Message& message) {
  FIX::ExecType exec_type;
  if (!message.getFieldIfSet(exec_type)) {
    return;
  }
  if (EXEC_TYPE_NEW_ORDER == exec_type || EXEC_TYPE_FILL == exec_type ||
      EXEC_TYPE_DONE == exec_type || EXEC_TYPE_CANCELED == exec_type ||
      EXEC_TYPE_STOPPED == exec_type || EXEC_TYPE_ORDER_CHANGED == exec_type) {
    FIX::OrderID order_id;
    FIX::Symbol symbol;
    FIX::Price price;
    FIX::ClOrdID client_id;
    FIX::Side side;
    FIX::OrdStatus order_status;
    message.getFieldIfSet(order_id);
    message.getFieldIfSet(symbol);
    message.getFieldIfSet(price);
    message.getFieldIfSet(side);
    message.getFieldIfSet(order_status);
    int64_t proc_order_id = 0;
    if (message.getFieldIfSet(client_id)) {
      if (auto poi = ToInt64(GetProcOrderId(client_id.getValue())); poi) {
        proc_order_id = *poi;
      }
    }

    ProductOrderBundle& product_order_bundle = *result_.mutable_order_list();
    product_order_bundle.Clear();
    auto order = product_order_bundle.add_each_order();
    order->Clear();
    order->set_market_type(MarketType::Spot);
    order->set_exchange_type(ExchangeType::Gdax);
    order->set_api_version("v1");
    order->set_exchange_order_id(order_id.getValue());
    order->set_proc_order_id(proc_order_id);
    order->set_symbol(symbol.getValue());
    order->set_price(price.getValue());
    // TODO(daniel) - different type
    double qty = 0.0;
    if (EXEC_TYPE_FILL == exec_type) {
      FIX::LastShares order_qty;
      message.getFieldIfSet(order_qty);
      qty = order_qty.getValue();
    } else {
      FIX::OrderQty order_qty;
      message.getFieldIfSet(order_qty);
      qty = order_qty.getValue();
    }
    order->set_qty(qty);

    if (BUY == side.getValue()) {
      order->set_side(coin::proto::OrderSide::BUY_ORDER);
    } else if (SELL == side.getValue()) {
      order->set_side(coin::proto::OrderSide::SELL_ORDER);
    }
    order->set_timestamp(msg_->timestamp);
    switch (order_status) {
      case ORD_STATUS_NEW_ORDER:
      case ORD_STATUS_PARTIALLY_FILLED:
      case ORD_STATUS_RESIZED_ORDER:
        order->set_is_live(true);
        break;
      case ORD_STATUS_FULLY_FILLED:
        order->set_fully_filled(true);
        break;
      case ORD_STATUS_CANCELLED_ORDER:
        order->set_cancel_confirmed(true);
        break;
      default:
        LOG(ERROR) << "[ParseFIXOrderInfo] Unknown order status! " << order_status;
        break;
    }
    if (EXEC_TYPE_STOPPED == exec_type) {
      LOG(WARNING) << "order [" << order_id.getValue()
                   << "] is stopped, order staus: " << order_status;
      order->set_is_live(false);
    }
    result_.set_type(MsgType::ORDER_STATUS);
  }
}

auto GdaxParser::ParseRawMessage(const RawMessage& msg) -> const ParseResult& {
  result_.Clear();
  msg_ = &msg;
  sub_topic_id_ = static_cast<OrderSubTopicId>(msg.type);

  if (msg.type == OrderSubTopicId::WS_MESSAGE) {
    ParseWsMessage();
    return result_;
  }

  if (msg.type == OrderSubTopicId::QUICKFIX_MESSAGE) {
    ParseFixMessage();
    return result_;
  }

  switch (msg.http_context->query_type) {
    case OrderSubTopicId::SUBMIT_RESPONSE:
      ParseSubmitResponse();
      break;
    case OrderSubTopicId::CANCEL_RESPONSE:
      ParseCancelResponse();
      break;
    case OrderSubTopicId::MULTI_CANCEL_RESPONSE:
      ParseMultiCancelResponse();
      break;
    case OrderSubTopicId::REST_OPEN_ORDERS:
    case OrderSubTopicId::REST_ALL_ORDERS:
      ParseRestOrdersInfo();
      break;
    case OrderSubTopicId::REST_FILL_ORDERS:
      ParseRestFillOrders();
      break;
    case OrderSubTopicId::REST_ACCOUNT_BALANCE:
      ParseRestAccountBalance();
      break;
    case GdaxOrderSubTopicId::REST_DEPOSIT:
      ParseRestDeposit();
      break;
    case GdaxOrderSubTopicId::REST_WITHDRAWAL:
      ParseRestWithdrawal();
      break;
    case GdaxOrderSubTopicId::REST_TRANSFERS_INIT:
      ParseRestTransfersInit();
      break;
    case OrderSubTopicId::TRANSFER:
      ParseRestTransfers(msg.http_context->extra_info);
      break;
    default: {
      LOG(ERROR) << "Parsing Error. " << msg_->DebugString();
      result_.set_error_code(OrderErrorCode::ORDER_UNKNOWN_ERROR);
      break;
    }
  }

  return result_;
}

}  // namespace coin2::exchange::gdax::order_v1
